/*
 * Copyright 2009-2010 the original author or authors.
 *
 * Licensed under the Apache license, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.internna.iwebjtracker.spring.mvc;

import java.util.Set;
import java.util.Map;
import java.util.List;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Calendar;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import org.internna.iwebmvc.dao.DAO;
import org.internna.iwebmvc.model.UUID;
import org.internna.iwebmvc.utils.Assert;
import org.internna.iwebmvc.model.ui.Filter;
import org.internna.iwebmvc.crypto.Decipherer;
import org.internna.iwebmvc.model.DomainEntity;
import org.internna.iwebmvc.utils.CollectionUtils;
import org.internna.iwebmvc.model.ui.PredefinedQuery;
import org.internna.iwebjtracker.issue.model.Issue;
import org.internna.iwebjtracker.model.RoadmapItem;
import org.internna.iwebjtracker.model.DefaultProject;
import org.internna.iwebjtracker.project.model.Version;
import org.internna.iwebjtracker.issue.model.Issue.Status;
import org.internna.iwebjtracker.sonar.model.SonarMetrics;
import org.internna.iwebjtracker.services.TrackerUserManager;
import org.internna.iwebjtracker.sonar.services.SonarManager;
import org.internna.iwebjtracker.hudson.services.HudsonManager;
import org.internna.iwebjtracker.sonar.model.SonarConfiguration;
import org.internna.iwebjtracker.hudson.model.HudsonConfiguration;
import org.internna.iwebjtracker.issue.model.Issue.ResolutionStatus;
import org.springframework.ui.ModelMap;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * Accepts requests to show the project main page.
 *
 * @author Jose Noheda
 * @since 1.0
 */
@Controller
@RequestMapping("/project.iwebmvc")
public final class ProjectController {

    private static final String STATUS = "status", PROJECT_ID = "projectId", VERSIONS = "versions";
    private static final String QUERY_UNASSIGNED = "SELECT COUNT(id) FROM DefaultIssue WHERE (parentProject.id = :" + PROJECT_ID + ") AND (targetVersion = null)";
    private static final String QUERY_BY_STATUS = "SELECT COUNT(id) FROM DefaultIssue WHERE (parentProject.id = :" + PROJECT_ID + ") AND (status = :" + STATUS + ")";
    private static final String QUERY_BY_NOT_STATUS = "SELECT COUNT(id) FROM DefaultIssue WHERE (parentProject.id = :" + PROJECT_ID + ") AND (status != :" + STATUS + ")";

    static final String ENTITY = "ENTITY";

    /**
     * Key (JSP) for the project
     */
    public static final String PROJECT = "project";

    /**
     * Key (JSP) for the statistics
     */
    public static final String VERSION_BY_STATUS = "versionByStatus";

    /**
     * Key (JSP) for the project
     */
    public static final String PROJECT_ADMIN = "isprojectadmin";

    /**
     * Key (JSP) for the roadmap panel
     */
    public static final String ROADMAP = "roadmap";

    /**
     * Key (JSP) for the Sonar configuration data
     */
    public static final String SONAR = "sonar";

    /**
     * Key (JSP) for the Hudson configuration data
     */
    public static final String HUDSON = "hudson";

    /**
     * Key (JSP) for the Sonar metrics data
     */
    public static final String METRICS = "metrics";

    /**
     * Key (JSP) for the Hudson build data
     */
    public static final String BUILD = "build";

    /**
     * Key (JSP) for the filters to apply to the issue grid
     */
    public static final String ISSUE_FILTERS = "issueFilters";

    /**
     * Key (JSP) for the filters to apply to the activity grid
     */
    public static final String ACTIVITY_FILTERS = "activityFilters";

    /**
     * Key (JSP) with custom queries to filter issues.
     */
    public static final String PREDEFINED_QUERIES = "predefinedQueries";

    private DAO dao;
    private SonarManager sonar;
    private HudsonManager hudson;
    private Decipherer decipherer;
    private final Set<String> collections;
    private TrackerUserManager userManager;

    /**
     * Establishes the DefaultProject collections: owners, versions, modules, developers
     */
    public ProjectController() {
        collections = new HashSet<String>();
        collections.add("owners");
        collections.add("versions");
        collections.add("modules");
        collections.add("developers");
    }

    /**
     * Spring injection point (REQUIRED)
     *
     * @param dao a not null persistence facade
     */
    @Required public void setDao(DAO dao) {
        Assert.notNull(dao);
        this.dao = dao;
    }

    /**
     * Spring injection point.
     *
     * @param sonar any
     */
    public void setSonar(SonarManager sonar) {
        this.sonar = sonar;
    }

    /**
     * Spring injection point.
     *
     * @param hudson any
     */
    public void setHudson(HudsonManager hudson) {
        this.hudson = hudson;
    }

    /**
     * Spring injection point (REQUIRED)
     *
     * @param decipherer a not null implementation
     */
    @Required public void setDecipherer(Decipherer decipherer) {
        Assert.notNull(decipherer);
        this.decipherer = decipherer;
    }

    /**
     * Spring injection point (REQUIRED)
     *
     * @param decipherer a not null implementation
     */
    @Required public void setUserManager(TrackerUserManager userManager) {
        Assert.notNull(userManager);
        this.userManager = userManager;
    }

    /**
     * Changes the objects and collections of the target entity that will be automatically
     * initialized in the view.
     *
     * @param collections any
     */
    public void setCollectionsToInitialize(Collection<String> collections) {
        this.collections.clear();
        if (CollectionUtils.isNotEmpty(collections)) {
            this.collections.addAll(collections);
        }
    }

    /**
     * Accessor method for testing purposes.
     */
    protected Set<String> getCollections() {
        return collections;
    }

    /**
     * Gets the project from its primary key and populates the model.
     *
     * @param uuid provided by the "project" URL parameter
     * @param model injected by Spring MVC automatically
     *
     * @return /project/index
     */
    @RequestMapping(method = RequestMethod.GET)
    public String fillModel(@RequestParam(value = "project", required = true) String uuid, ModelMap model) {
        Assert.isEncrypted(decipherer, uuid);
        DefaultProject project = loadProject(decipherer.decrypt(uuid));
        model.addAttribute(PROJECT, project);
        model.addAttribute(ROADMAP, getRoadmap(project));
        SonarConfiguration sonarConfiguration = loadSonar(project.getId());
        if (sonarConfiguration != null) {
            model.addAttribute(SONAR, sonarConfiguration);
            SonarMetrics metrics = sonar.getMetricData(sonarConfiguration, getSonarMetrics());
            model.addAttribute(METRICS, metrics == null ? null : metrics.getMetrics());
        }
        HudsonConfiguration hudsonConfiguration = loadHudson(project.getId());
        if (hudsonConfiguration != null) {
            model.addAttribute(HUDSON, hudsonConfiguration);
            model.addAttribute(BUILD, hudson.getInformationForLastBuild(hudsonConfiguration));
        }
        model.addAttribute(ISSUE_FILTERS, generateIssueFilters(uuid));
        model.addAttribute(ACTIVITY_FILTERS, generateActivityFilters(uuid));
        model.addAttribute(PREDEFINED_QUERIES, getPredefinedProjectQueries(uuid));
        model.addAttribute(VERSION_BY_STATUS, obtainCurrentVersionStatusStatistics(project));
        model.addAttribute(PROJECT_ADMIN, project.isAdministrator(userManager.getCurrentTrackerUser()));
        return "/project/index";
    }

    /**
     * Retrieves a project from database.
     *
     * @param uuid a not null encrypted primary key
     * @return any
     */
    protected DefaultProject loadProject(String uuid) {
        DefaultProject project = dao.find(DefaultProject.class, new UUID(uuid));
        return CollectionUtils.isEmpty(collections) ? project : dao.initialize(project, collections.toArray(new String[collections.size()]));
    }

    /**
     * Generates a valid list of filters to apply to a issue grid.
     *
     * @param project a non null project
     * @return a not empty list
     */
    protected List<Filter> generateIssueFilters(String encUUID) {
        List<Filter> filters = new ArrayList<Filter>();
        Filter projectFilter = new Filter();
        projectFilter.setRange(false);
        projectFilter.setType(ENTITY);
        projectFilter.setPath("parentProject.id");
        projectFilter.setFrom(encUUID);
        filters.add(projectFilter);
        return Collections.unmodifiableList(filters);
    }

    /**
     * Generates a valid list of filters to apply to a grid that filters
     * activity for this project.
     *
     * @param project a non null project
     * @return a not empty list
     */
    protected List<Filter> generateActivityFilters(String encUUID) {
        List<Filter> filters = new ArrayList<Filter>();
        Filter projectFilter = new Filter();
        projectFilter.setRange(false);
        projectFilter.setType(ENTITY);
        projectFilter.setPath("project.id");
        projectFilter.setFrom(encUUID);
        filters.add(projectFilter);
        return Collections.unmodifiableList(filters);
    }

    /**
     * Obtains an ordered collection of all the pending issues:
     *
     *   1) Of the project
     *   2) Of every version of the project
     *   3) Of each milestone of each version
     *
     * As long as it hasn't been completed
     *
     * @param project
     * @return
     */
    protected List<RoadmapItem> getRoadmap(DefaultProject project) {
        List<RoadmapItem> items = new ArrayList<RoadmapItem>();
        if (project != null) {
            RoadmapItem item = calculateProjectRoadmap(project);
            if (item != null) {
                items.add(item);
                items.add(calculateVersionRoadmap(project, true));
                items.add(calculateVersionRoadmap(project, false));
                items.add(calculateUnassigned(project));
            }
        }
        return Collections.unmodifiableList(items);
    }

    private RoadmapItem calculateProjectRoadmap(DefaultProject project) {
        RoadmapItem item = null;
        Map<String, Object> params = new HashMap<String, Object>(2);
        params.put(PROJECT_ID, project.getId());
        params.put(STATUS, Issue.Status.CLOSED);
        Long open = (Long) dao.executeQuery(QUERY_BY_NOT_STATUS, 0, 99999, params).get(0);
        if (open > 0) {
            item = new RoadmapItem();
            item.setName(project.getProjectName());
            item.setActive(open);
            item.setClosed((Long) dao.executeQuery(QUERY_BY_STATUS, 0, 99999, params).get(0));
        }
        return item;
    }

    private RoadmapItem calculateVersionRoadmap(DefaultProject project, boolean current) {
        RoadmapItem item = new RoadmapItem();
        Version version = project.getCurrentVersion();
        if (version instanceof DomainEntity && !version.isClosed()) {
            item.setDue(version.getDueDate());
            item.setName(version.getLabel());
            Map<String, Object> params = new HashMap<String, Object>(2);
            params.put(PROJECT_ID, project.getId());
            params.put(STATUS, Issue.Status.CLOSED);
            params.put(VERSIONS, getVersions(version, current));
            item.setActive((Long) dao.executeQuery(QUERY_BY_NOT_STATUS + " AND (targetVersion != null) AND (targetVersion.id " + (current ? "" : "NOT") + " IN (:" + VERSIONS + "))", 0, 99999, params).get(0));
            item.setClosed((Long) dao.executeQuery(QUERY_BY_STATUS + " AND (targetVersion != null) AND (targetVersion.id " + (current ? "" : "NOT") + " IN (:" + VERSIONS + "))", 0, 99999, params).get(0));
        }
        return item;
    }

    private List<UUID> getVersions(Version version, boolean current) {
        List<UUID> versions = new ArrayList<UUID>();
        versions.add(((DomainEntity) version).getId());
        Collection<Version> milestones = version.getMilestones();
        if (CollectionUtils.isNotEmpty(milestones)) {
            for (Version milestone : milestones) {
                if (milestone instanceof DomainEntity && (!milestone.isClosed() || !current)) {
                    versions.add(((DomainEntity) milestone).getId());
                }
            }
        }
        return versions;
    }

    protected RoadmapItem calculateUnassigned(DefaultProject project) {
        RoadmapItem item = new RoadmapItem();
        Map<String, Object> params = new HashMap<String, Object>(2);
        params.put(PROJECT_ID, project.getId());
        item.setActive((Long) dao.executeQuery(QUERY_UNASSIGNED, 0, 99999, params).get(0));
        item.setClosed(0L);
        return item;
    }

    protected List<Set<Map.Entry<?, Long>>> obtainCurrentVersionStatusStatistics(DefaultProject project) {
        List<Set<Map.Entry<?, Long>>> statistics = new ArrayList<Set<Map.Entry<?, Long>>>(2);
        statistics.add(new HashSet<Map.Entry<?, Long>>());
        statistics.add(new HashSet<Map.Entry<?, Long>>());
        if (project != null) {
            Map<String, Object> params = new HashMap<String, Object>();
            params.put(VERSIONS, project.getCurrentVersion());
            final List<Object[]> byStatus = (List) dao.executeQuery("SELECT status, count(id) from DefaultIssue WHERE targetVersion = :" + VERSIONS + " GROUP BY status", 0, 99999, params);
            if (CollectionUtils.isNotEmpty(byStatus)) {
                for (long index = 0; index < byStatus.size(); index++) {
                    Map.Entry<Status, Long> entry = new Map.Entry<Status, Long>() {
                        private int element;

                        @Override public Status getKey() {
                            return (Status) byStatus.get(element)[0];
                        }

                        @Override public Long getValue() {
                            return (Long) byStatus.get(element)[1];
                        }

                        @Override public Long setValue(Long value) {
                            element = value.intValue();
                            return getValue();
                        }
                    };
                    entry.setValue(index);
                    statistics.get(0).add(entry);
                }
            }
            final List<Object[]> byResolution = (List) dao.executeQuery("SELECT resolutionStatus, count(id) from DefaultIssue WHERE targetVersion = :" + VERSIONS + " GROUP BY resolutionStatus", 0, 99999, params);
            if (CollectionUtils.isNotEmpty(byResolution)) {
                for (long index = 0; index < byResolution.size(); index++) {
                    Map.Entry<ResolutionStatus, Long> entry = new Map.Entry<ResolutionStatus, Long>() {
                        private int element;

                        @Override public ResolutionStatus getKey() {
                            return (ResolutionStatus) byResolution.get(element)[0];
                        }

                        @Override public Long getValue() {
                            return (Long) byResolution.get(element)[1];
                        }

                        @Override public Long setValue(Long value) {
                            element = value.intValue();
                            return getValue();
                        }
                    };
                    entry.setValue(index);
                    statistics.get(1).add(entry);
                }
            }
        }
        return statistics;
    }

    protected Collection<PredefinedQuery> getPredefinedProjectQueries(String id) {
        Collection<PredefinedQuery> queries = new ArrayList<PredefinedQuery>();
        queries.add(getRecentIssuesQuery(id));
        queries.add(getAllOpenQuery(id));
        queries.add(getShowStoppersQuery(id));
        queries.add(getUnassignedQuery(id));
        queries.add(getMissingTargetQuery(id));
        return queries;
    }

    protected PredefinedQuery getAllOpenQuery(String id) {
        PredefinedQuery allOpen = new PredefinedQuery();
        allOpen.setKey("project.queries.allOpen");
        Filter filter = new Filter();
        filter.setType("ENUM");
        filter.setInvert(true);
        filter.setPath("status");
        filter.setFrom(String.valueOf(Status.CLOSED.ordinal()));
        allOpen.addFilter(filter);
        allOpen.addFilter(getThisProjectFilter(id));
        return allOpen;
    }

    protected PredefinedQuery getUnassignedQuery(String id) {
        PredefinedQuery allUnassigned = new PredefinedQuery();
        allUnassigned.setKey("project.queries.allWithoutAssignee");
        Filter filter = new Filter();
        filter.setNullValue(true);
        filter.setPath("assignee");
        allUnassigned.addFilter(filter);
        allUnassigned.addFilter(getThisProjectFilter(id));
        return allUnassigned;
    }

    protected PredefinedQuery getMissingTargetQuery(String id) {
        PredefinedQuery allUnassigned = new PredefinedQuery();
        allUnassigned.setKey("project.queries.allWithoutTarget");
        Filter filter = new Filter();
        filter.setNullValue(true);
        filter.setPath("targetVersion");
        allUnassigned.addFilter(filter);
        allUnassigned.addFilter(getThisProjectFilter(id));
        return allUnassigned;
    }

    protected PredefinedQuery getShowStoppersQuery(String id) {
        PredefinedQuery allBlocker = new PredefinedQuery();
        allBlocker.setKey("project.queries.showStoppers");
        Filter filter = new Filter();
        filter.setType("ENUM");
        filter.setPath("severity");
        filter.setFrom(String.valueOf(Issue.Severity.BLOCKER.ordinal()));
        allBlocker.addFilter(filter);
        allBlocker.addFilter(getThisProjectFilter(id));
        return allBlocker;
    }

    protected PredefinedQuery getRecentIssuesQuery(String id) {
        PredefinedQuery allRecent = new PredefinedQuery();
        allRecent.setKey("project.queries.recent");
        Filter filter = new Filter();
        filter.setType("DATE");
        filter.setRange(true);
        Calendar calendar = Calendar.getInstance();
        filter.setTo(String.valueOf(calendar.getTimeInMillis()));
        calendar.add(Calendar.DAY_OF_YEAR, -3);
        filter.setFrom(String.valueOf(calendar.getTimeInMillis()));
        filter.setPath("creationDate");
        allRecent.addFilter(filter);
        allRecent.addFilter(getThisProjectFilter(id));
        return allRecent;
    }

    /**
     * Creates a filter that points to this project in database.
     *
     * @param id any
     * @return a non null instance
     */
    protected Filter getThisProjectFilter(String id) {
        Filter filter = new Filter();
        filter.setType("ENTITY");
        filter.setPath("parentProject.id");
        filter.setFrom(id);
        return filter;
    }

    private Map<String, Object> getProjectParams(UUID project) {
        Map<String, Object> params = new HashMap<String, Object>(1);
        params.put("project", project);
        return params;
    }

    /**
     * Loads the Sonar configuration for this project if it already exists.
     *
     * @param project any
     * @return any
     */
    protected SonarConfiguration loadSonar(UUID project) {
        if (project != null) {
            List<SonarConfiguration> conf = dao.findByNamedQuery(SonarConfiguration.FIND_BY_PROJECT, 0, 1, getProjectParams(project));
            if (CollectionUtils.isNotEmpty(conf)) {
                return conf.get(0);
            }
        }
        return null;
    }

    /**
     * The metrics to retrieve from a Sonar installation
     *
     * @return
     */
    protected Set<SonarManager.METRICS> getSonarMetrics() {
        Set<SonarManager.METRICS> metrics = new HashSet<SonarManager.METRICS>();
        metrics.add(SonarManager.METRICS.UNIT_TESTS);
        metrics.add(SonarManager.METRICS.TEST_COVERAGE);
        metrics.add(SonarManager.METRICS.DOCUMENTED_API);
        metrics.add(SonarManager.METRICS.RULES_VIOLATIONS);
        metrics.add(SonarManager.METRICS.BLOCKER_VIOLATIONS);
        metrics.add(SonarManager.METRICS.CRITICAL_VIOLATIONS);
        metrics.add(SonarManager.METRICS.LINES_OF_CODE_WITHOUT_COMMENTS);
        return metrics;
    }

    /**
     * Loads the Hudson configuration for this project if it already exists.
     *
     * @param project any
     * @return any
     */
    protected HudsonConfiguration loadHudson(UUID project) {
        if (project != null) {
            List<HudsonConfiguration> conf = dao.findByNamedQuery(HudsonConfiguration.FIND_BY_PROJECT, 0, 1, getProjectParams(project));
            if (CollectionUtils.isNotEmpty(conf)) {
                return conf.get(0);
            }
        }
        return null;
    }

}
